{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "civil-refund",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 417 ms (started: 2021-08-17 21:30:43 +08:00)\n"
     ]
    }
   ],
   "source": [
    "# 自动计算cell的计算时间\n",
    "%load_ext autotime\n",
    "\n",
    "%matplotlib inline\n",
    "%config InlineBackend.figure_format='svg' #矢量图设置，让绘图更清晰"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 78,
   "id": "monthly-bottle",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "origin\tgit@github.com:ustchope/handson-ml2.git (fetch)\n",
      "origin\tgit@github.com:ustchope/handson-ml2.git (push)\n",
      "[master e2ea23f] 更新 ch12 #1 全部更新 Aug 18, 2021\n",
      " 1 file changed, 3136 insertions(+), 13 deletions(-)\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "To git@github.com:ustchope/handson-ml2.git\n",
      "   41ee6de..e2ea23f  master -> master\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 4.29 s (started: 2021-08-18 00:29:56 +08:00)\n"
     ]
    }
   ],
   "source": [
    "%%bash\n",
    "\n",
    "# 增加更新\n",
    "git add *.ipynb *.md\n",
    "\n",
    "git remote -v\n",
    "\n",
    "git commit -m '更新 ch12 #1 全部更新 Aug 18, 2021'\n",
    "\n",
    "git push origin master"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "worse-steam",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 2.09 s (started: 2021-08-17 21:30:46 +08:00)\n"
     ]
    }
   ],
   "source": [
    "#设置使用的gpu\n",
    "import tensorflow as tf\n",
    "\n",
    "gpus = tf.config.list_physical_devices(\"GPU\")\n",
    "\n",
    "if gpus:\n",
    "   \n",
    "    gpu0 = gpus[0] #如果有多个GPU，仅使用第0个GPU\n",
    "    tf.config.experimental.set_memory_growth(gpu0, True) #设置GPU显存用量按需使用\n",
    "    # 或者也可以设置GPU显存为固定使用量(例如：4G)\n",
    "    #tf.config.experimental.set_virtual_device_configuration(gpu0,\n",
    "    #    [tf.config.experimental.VirtualDeviceConfiguration(memory_limit=4096)]) \n",
    "    tf.config.set_visible_devices([gpu0],\"GPU\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "animal-newark",
   "metadata": {},
   "source": [
    "# 使用 TensorFlow 定制模型和训练"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "touched-asset",
   "metadata": {},
   "source": [
    "到目前为止，我们只使用了 TensorFlow 的高级 API tf.keras，但它已经让我们走得很远：我们构建了各种神经网络架构，包括回归和分类网络、Wide & Deep 网络以及自标准化网络，使用各种技术，例如批量标准化、辍学和学习率计划。事实上，你将遇到的 95% 的用例除了 tf.keras（和 tf.data；参见第 13 章）之外不需要任何其他东西。但是现在是时候深入研究 TensorFlow 并查看其较低级别的 Python API。当您需要额外的控制来编写自定义损失函数、自定义指标、层、模型、初始化程序、正则化程序、权重约束等时，这将非常有用。您甚至可能需要完全控制训练循环本身，例如对梯度应用特殊转换或约束（不仅仅是裁剪它们）或对网络的不同部分使用多个优化器。我们将在本章中介绍所有这些情况，我们还将研究如何使用 TensorFlow 的自动图形生成功能来提升您的自定义模型和训练算法。但首先，让我们快速浏览一下 TensorFlow。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "engaging-society",
   "metadata": {},
   "source": [
    "## TensorFlow 快速浏览"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "intense-grocery",
   "metadata": {},
   "source": [
    "如您所知，TensorFlow 是一个强大的数值计算库，特别适合大规模机器学习并进行了微调（但您可以将它用于需要大量计算的其他任何事情）。 它由 Google Brain 团队开发，为 Google 的许多大型服务提供支持，例如 Google Cloud Speech、Google Photos 和 Google Search。 它于 2015 年 11 月开源，现在是最受欢迎的深度学习库（就论文引用、公司采用率、GitHub 上的明星等而言）。 无数项目将 TensorFlow 用于各种机器学习任务，例如图像分类、自然语言处理、推荐系统和时间序列预测。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "incorrect-badge",
   "metadata": {},
   "source": [
    "那么 TensorFlow 提供了什么？ 这是一个总结：\n",
    "* 其核心与NumPy 非常相似，但具有GPU 支持。\n",
    "* 支持分布式计算（跨多个设备和服务器）。\n",
    "* 它包括一种即时 (JIT) 编译器，允许它针对速度和内存使用情况优化计算。 它的工作原理是从 Python 函数中提取计算图，然后对其进行优化（例如，通过修剪未使用的节点），最后有效地运行它（例如，通过自动并行运行独立操作）。\n",
    "* 计算图可以导出为可移植格式，因此您可以在一种环境中训练 TensorFlow 模型（例如，在 Linux 上使用 Python）并在另一种环境中运行它（例如，在 Android 设备上使用 Java）。\n",
    "* 它实现了 autodiff（参见第 10 章和附录 D）并提供了一些优秀的优化器，例如 RMSProp 和 Nadam（参见第 11 章），因此您可以轻松地最小化各种损失函数。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "pursuant-holly",
   "metadata": {},
   "source": [
    "TensorFlow 提供了更多基于这些核心功能构建的功能：最重要的当然是 tf.keras，但它也有数据加载和预处理操作（tf.data、tf.io 等）、图像处理操作（tf .image)、信号处理操作 (tf.signal) 等（有关 TensorFlow Python API 的概述，请参见图 12-1）。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "comfortable-court",
   "metadata": {},
   "source": [
    "> 我们将介绍 TensorFlow API 的许多包和功能，但不可能全部介绍，因此您确实应该花一些时间浏览 API； 你会发现它非常丰富并且有据可查。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "beginning-vocabulary",
   "metadata": {},
   "source": [
    "![](https://tva1.sinaimg.cn/large/008i3skNly1gtjqvy6yk5j61120qwtd502.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "lonely-halifax",
   "metadata": {},
   "source": [
    "在最底层，每个 TensorFlow 操作（简称 op）都是使用高效的 C++ 代码实现的。 许多操作有多个称为内核的实现：每个内核专用于特定的设备类型，例如 CPU、GPU，甚至 TPU（张量处理单元）。 您可能知道，GPU 可以通过将它们分成许多较小的块并在许多 GPU 线程上并行运行它们来显着加快计算速度。 TPU 甚至更快：它们是专门为深度学习操作构建的定制 ASIC 芯片3（我们将在第 19 章中讨论如何将 TensorFlow 与 GPU 或 TPU 结合使用）。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "residential-legislature",
   "metadata": {},
   "source": [
    "TensorFlow 的架构如图 12-2 所示。 大多数情况下，您的代码将使用高级 API（尤其是 tf.keras 和 tf.data）； 但是当您需要更大的灵活性时，您将使用较低级别的 Python API，直接处理张量。 请注意，其他语言的 API 也可用。 在任何情况下，TensorFlow 的执行引擎都将负责高效地运行操作，如果您告诉它，甚至可以跨多个设备和机器运行。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "multiple-magazine",
   "metadata": {},
   "source": [
    "![](https://tva1.sinaimg.cn/large/008i3skNgy1gtk3qjocaij61fy0mywhn02.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "respiratory-military",
   "metadata": {},
   "source": [
    "TensorFlow 不仅可以在 Windows、Linux 和 macOS 上运行，还可以在移动设备（使用 TensorFlow Lite）上运行，包括 iOS 和 Android（参见第 19 章）。 如果您不想使用 Python API，则可以使用 C++、Java、Go 和 Swift API。 甚至还有一个名为 TensorFlow.js 的 JavaScript 实现，可以直接在浏览器中运行模型。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "postal-proceeding",
   "metadata": {},
   "source": [
    "TensorFlow 不仅仅是库。 TensorFlow 是一个广泛的库生态系统的中心。 首先，有用于可视化的 TensorBoard（参见第 10 章）。 接下来是 TensorFlow Extended (TFX)，这是一组由 Google 构建的用于生产 TensorFlow 项目的库：它包括用于数据验证、预处理、模型分析和服务（使用 TF Serving；参见第 19 章）的工具。 Goo gle 的 TensorFlow Hub 提供了一种轻松下载和重用预训练神经网络的方法。 您还可以在 TensorFlow 的模型花园中获得许多神经网络架构，其中一些是经过预训练的。 查看 TensorFlow 资源和 https://github.com/jtoy/awesome-tensorflow 以了解更多基于 TensorFlow 的项目。 您会在 GitHub 上找到数百个 TensorFlow 项目，因此通常很容易找到适合您尝试执行的任何操作的现有代码。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "social-paintball",
   "metadata": {},
   "source": [
    "最后但并非最不重要的一点是，TensorFlow 拥有一支由热情和乐于助人的开发人员组成的专门团队，以及一个致力于改进它的大型社区。 要提出技术问题，您应该使用 http://stackoverflow.com/ 并使用 tensorflow 和 python 标记您的问题。 您可以通过 GitHub 提交错误和功能请求。 对于一般性讨论，请加入 Google 群组。\n",
    "\n",
    "好的，是时候开始编码了！"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "integrated-active",
   "metadata": {},
   "source": [
    "## 像 NumPy 一样使用 TensorFlow"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "false-sacrifice",
   "metadata": {},
   "source": [
    "TensorFlow 的 API 围绕着张量展开，张量从一个操作流向另一个操作——因此得名 TensorFlow。 张量与 NumPy ndarray 非常相似：它通常是一个多维数组，但它也可以保存一个标量（一个简单的值，例如 42）。 当我们创建自定义成本函数、自定义指标、自定义层等时，这些张量将很重要，所以让我们看看如何创建和操作它们。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "built-celebration",
   "metadata": {},
   "source": [
    "### 张量和操作"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "generous-greece",
   "metadata": {},
   "source": [
    "您可以使用 tf.constant() 创建张量。 例如，这里是一个表示具有两行三列浮点数的矩阵的张量："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "judicial-thanksgiving",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<tf.Tensor: shape=(2, 3), dtype=float32, numpy=\n",
       "array([[1., 2., 3.],\n",
       "       [4., 5., 6.]], dtype=float32)>"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 1.43 s (started: 2021-08-17 21:30:51 +08:00)\n"
     ]
    }
   ],
   "source": [
    "tf.constant([[1., 2., 3.], [4., 5., 6.]]) # matrix"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "imperial-opinion",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<tf.Tensor: shape=(), dtype=int32, numpy=42>"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 3.51 ms (started: 2021-08-17 21:31:08 +08:00)\n"
     ]
    }
   ],
   "source": [
    "tf.constant(42) # scalar"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "duplicate-expression",
   "metadata": {},
   "source": [
    "就像一个 ndarray，一个 tf.Tensor 有一个形状和一个数据类型（dtype）："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "exact-blade",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 773 µs (started: 2021-08-17 21:31:45 +08:00)\n"
     ]
    }
   ],
   "source": [
    "t = tf.constant([[1., 2., 3.], [4., 5., 6.]])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "popular-phoenix",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "TensorShape([2, 3])"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 4.29 ms (started: 2021-08-17 21:31:48 +08:00)\n"
     ]
    }
   ],
   "source": [
    "t.shape"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "romantic-conservation",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "tf.float32"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 2.84 ms (started: 2021-08-17 21:31:56 +08:00)\n"
     ]
    }
   ],
   "source": [
    "t.dtype"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "suspected-penny",
   "metadata": {},
   "source": [
    "索引的工作方式与 NumPy 非常相似："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "peaceful-sunglasses",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<tf.Tensor: shape=(2, 2), dtype=float32, numpy=\n",
       "array([[2., 3.],\n",
       "       [5., 6.]], dtype=float32)>"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 21.6 ms (started: 2021-08-17 21:32:30 +08:00)\n"
     ]
    }
   ],
   "source": [
    "t[:, 1:]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "round-granny",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<tf.Tensor: shape=(2, 1), dtype=float32, numpy=\n",
       "array([[2.],\n",
       "       [5.]], dtype=float32)>"
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 7.94 ms (started: 2021-08-17 21:32:51 +08:00)\n"
     ]
    }
   ],
   "source": [
    "t[..., 1, tf.newaxis]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "middle-borough",
   "metadata": {},
   "source": [
    "最重要的是，可以使用各种张量操作："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "substantial-signal",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<tf.Tensor: shape=(2, 3), dtype=float32, numpy=\n",
       "array([[11., 12., 13.],\n",
       "       [14., 15., 16.]], dtype=float32)>"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 4.18 ms (started: 2021-08-17 21:33:38 +08:00)\n"
     ]
    }
   ],
   "source": [
    "t + 10"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "crude-norfolk",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<tf.Tensor: shape=(2, 3), dtype=float32, numpy=\n",
       "array([[ 1.,  4.,  9.],\n",
       "       [16., 25., 36.]], dtype=float32)>"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 3.77 ms (started: 2021-08-17 21:34:02 +08:00)\n"
     ]
    }
   ],
   "source": [
    "tf.square(t)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "instructional-architect",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<tf.Tensor: shape=(2, 2), dtype=float32, numpy=\n",
       "array([[14., 32.],\n",
       "       [32., 77.]], dtype=float32)>"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 1.6 s (started: 2021-08-17 21:34:11 +08:00)\n"
     ]
    }
   ],
   "source": [
    "t @ tf.transpose(t)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "regulation-nutrition",
   "metadata": {},
   "source": [
    "请注意，写 `t + 10` 相当于调用 `tf.add(t, 10)` （实际上，Python 调用了魔术方法 `t.__add__(10)`，它只是调用了 `tf.add(t, 10)`）。 还支持其他运算符，如 `-` 和 `*`。 `@` 运算符是在 Python 3.5 中添加的，用于矩阵乘法：它等效于调用 `tf.matmul()` 函数。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "instant-mistress",
   "metadata": {},
   "source": [
    "你会发现你需要的所有基本数学运算（`tf.add()、tf.multiply()、tf.square()、tf.exp()、tf.sqrt()`等）和你能做的大多数运算在 NumPy 中找到（例如，`tf.reshape()、tf.squeeze()、tf.tile()）`。某些函数的名称与 NumPy 中的名称不同；例如，`tf.reduce_mean()、tf.reduce_sum()、tf.reduce_max()` 和 `tf.math.log()` 等价于 `np.mean()、np.sum()、np.max()和 np.log()`。当名称不同时，通常有一个很好的理由。例如，在 TensorFlow 中你必须编写 `tf.transpose(t)`;你不能像在 NumPy 中那样写 `t.T`。原因是 `tf.transpose()` 函数和 NumPy 的 T 属性做的事情并不完全一样：在 TensorFlow 中，一个新的张量是用它自己的转置数据副本创建的，而在 NumPy 中，tT 只是一个转置视图在相同的数据上。类似地，`tf.reduce_sum()` 操作之所以这样命名是因为它的 GPU 内核（即 GPU 实现）使用了不保证元素添加顺序的 `reduce` 算法：因为 32 位浮点数的精度有限，每次调用此操作时，结果可能会略有变化。 `tf.reduce_mean()` 也是如此（当然 `tf.reduce_max()` 是确定性的）。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "intermediate-harrison",
   "metadata": {},
   "source": [
    "> 许多函数和类都有别名。 例如， `tf.add()`和 `tf.math.add()` 是同一个函数。 这允许 TensorFlow 为最常见的操作提供简洁的名称，同时保留组织良好的包。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dutch-stanford",
   "metadata": {},
   "source": [
    "> Keras 的底层 API"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "twenty-analysis",
   "metadata": {},
   "source": [
    "Keras API 有自己的低级 API，位于 `keras.backend`。 它包括 `square()`、`exp()` 和 `sqrt()` 等函数。 在 `tf.keras` 中，这些函数一般只是调用相应的 TensorFlow 操作。 如果您想编写可移植到其他 Keras 实现的代码，您应该使用这些 Keras 函数。 然而，它们只涵盖了 TensorFlow 中所有可用函数的一个子集，因此在本书中我们将直接使用 TensorFlow 操作。 这是使用 `keras.backend` 的简单示例，通常简称为 K："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "chicken-forth",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<tf.Tensor: shape=(3, 2), dtype=float32, numpy=\n",
       "array([[11., 26.],\n",
       "       [14., 35.],\n",
       "       [19., 46.]], dtype=float32)>"
      ]
     },
     "execution_count": 19,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 15.1 ms (started: 2021-08-17 21:41:27 +08:00)\n"
     ]
    }
   ],
   "source": [
    "from tensorflow import keras\n",
    "\n",
    "K = keras.backend\n",
    "\n",
    "K.square(K.transpose(t)) + 10"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "higher-stick",
   "metadata": {},
   "source": [
    "### 张量和 NumPy"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "diagnostic-audio",
   "metadata": {},
   "source": [
    "张量与 NumPy 配合得很好：您可以从 NumPy 数组创建张量，反之亦然。 您甚至可以将 TensorFlow 操作应用于 NumPy 数组，将 NumPy 操作应用于张量："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "id": "structured-functionality",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<tf.Tensor: shape=(3,), dtype=float64, numpy=array([2., 4., 5.])>"
      ]
     },
     "execution_count": 21,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 4.23 ms (started: 2021-08-17 21:43:52 +08:00)\n"
     ]
    }
   ],
   "source": [
    "import numpy as np\n",
    "\n",
    "a = np.array([2., 4., 5.])\n",
    "tf.constant(a)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "id": "renewable-analysis",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 1.06 ms (started: 2021-08-17 21:44:18 +08:00)\n"
     ]
    }
   ],
   "source": [
    "import tensorflow.experimental.numpy as tnp"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "id": "genetic-brief",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([[1., 2., 3.],\n",
       "       [4., 5., 6.]], dtype=float32)"
      ]
     },
     "execution_count": 26,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 3.97 ms (started: 2021-08-17 21:50:10 +08:00)\n"
     ]
    }
   ],
   "source": [
    "t.numpy() # or np.array(t)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "id": "roman-return",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<tf.Tensor: shape=(3,), dtype=float64, numpy=array([ 4., 16., 25.])>"
      ]
     },
     "execution_count": 28,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 5.4 ms (started: 2021-08-17 21:50:27 +08:00)\n"
     ]
    }
   ],
   "source": [
    "tf.square(a)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "id": "current-immune",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([[ 1.,  4.,  9.],\n",
       "       [16., 25., 36.]], dtype=float32)"
      ]
     },
     "execution_count": 29,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 3.6 ms (started: 2021-08-17 21:50:36 +08:00)\n"
     ]
    }
   ],
   "source": [
    "np.square(t)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "id": "falling-great",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<tf.Tensor: shape=(2, 3), dtype=float32, numpy=\n",
       "array([[ 1.,  4.,  9.],\n",
       "       [16., 25., 36.]], dtype=float32)>"
      ]
     },
     "execution_count": 30,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 7.21 ms (started: 2021-08-17 21:50:54 +08:00)\n"
     ]
    }
   ],
   "source": [
    "tnp.square(t)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "light-scene",
   "metadata": {},
   "source": [
    "> 请注意，NumPy 默认使用 64 位精度，而 TensorFlow 使用 32 位精度。 这是因为 32 位精度对于神经网络来说通常绰绰有余，而且它运行速度更快，使用的 RAM 更少。 因此，当您从 NumPy 数组创建张量时，请确保设置 dtype=tf.float32。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "jewish-tooth",
   "metadata": {},
   "source": [
    "### 类型转换"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "younger-pricing",
   "metadata": {},
   "source": [
    "类型转换会显着影响性能，而且当它们自动完成时很容易被忽视。 为避免这种情况，TensorFlow 不会自动执行任何类型转换：如果您尝试对类型不兼容的张量执行操作，它只会引发异常。 例如，您不能添加浮点张量和整数张量，甚至无法添加 32 位浮点数和 64 位浮点数："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cleared-paris",
   "metadata": {},
   "outputs": [],
   "source": [
    "tf.constant(2.) + tf.constant(40)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "floral-spanking",
   "metadata": {},
   "outputs": [],
   "source": [
    "tf.constant(2.) + tf.constant(40., dtype=tf.float64)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bored-beach",
   "metadata": {},
   "source": [
    "一开始这可能有点烦人，但请记住，这是有充分理由的！ 当然，当您确实需要转换类型时，您可以使用 tf.cast()："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "id": "paperback-attachment",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<tf.Tensor: shape=(), dtype=float32, numpy=42.0>"
      ]
     },
     "execution_count": 33,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 16.6 ms (started: 2021-08-17 21:54:46 +08:00)\n"
     ]
    }
   ],
   "source": [
    "t2 = tf.constant(40., dtype=tf.float64)\n",
    "\n",
    "tf.constant(2.0) + tf.cast(t2, tf.float32)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cellular-quarter",
   "metadata": {},
   "source": [
    "### 变量"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "french-change",
   "metadata": {},
   "source": [
    "到目前为止我们看到的 tf.Tensor 值是不可变的：你不能修改它们。 这意味着我们不能使用常规张量在神经网络中实现权重，因为它们需要通过反向传播进行调整。 此外，其他参数也可能需要随时间改变（例如，动量优化器会跟踪过去的梯度）。 我们需要的是一个 tf.Variable："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 34,
   "id": "weird-hotel",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<tf.Variable 'Variable:0' shape=(2, 3) dtype=float32, numpy=\n",
       "array([[1., 2., 3.],\n",
       "       [4., 5., 6.]], dtype=float32)>"
      ]
     },
     "execution_count": 34,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 12.4 ms (started: 2021-08-17 21:55:45 +08:00)\n"
     ]
    }
   ],
   "source": [
    "v = tf.Variable([[1., 2., 3.], [4., 5., 6.]])\n",
    "v"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "african-fundamentals",
   "metadata": {},
   "source": [
    "tf.Variable 的行为很像 tf.Tensor：你可以用它执行相同的操作，它也可以很好地与 NumPy 一起使用，而且它对类型也很挑剔。 但也可以使用`assign()` 方法（或`assign_add()` 或`assign_sub()`，按给定值递增或递减变量）就地修改它。 您还可以修改单个单元格（或切片），通过使用单元格（或切片）的 `assign()` 方法（直接项目分配不起作用）或使用 `scatter_update()` 或 `scatter_nd_update()`方法："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 35,
   "id": "lovely-printer",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<tf.Variable 'Variable:0' shape=(2, 3) dtype=float32, numpy=\n",
       "array([[ 2.,  4.,  6.],\n",
       "       [ 8., 10., 12.]], dtype=float32)>"
      ]
     },
     "execution_count": 35,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 18.7 ms (started: 2021-08-17 21:57:08 +08:00)\n"
     ]
    }
   ],
   "source": [
    "v.assign(2 * v)\n",
    "v"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "id": "heard-portal",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<tf.Variable 'UnreadVariable' shape=(2, 3) dtype=float32, numpy=\n",
       "array([[ 2., 42.,  6.],\n",
       "       [ 8., 10., 12.]], dtype=float32)>"
      ]
     },
     "execution_count": 36,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 9.1 ms (started: 2021-08-17 21:57:15 +08:00)\n"
     ]
    }
   ],
   "source": [
    "v[0, 1].assign(42)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 37,
   "id": "spiritual-violence",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<tf.Variable 'UnreadVariable' shape=(2, 3) dtype=float32, numpy=\n",
       "array([[ 2., 42.,  0.],\n",
       "       [ 8., 10.,  1.]], dtype=float32)>"
      ]
     },
     "execution_count": 37,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 8.27 ms (started: 2021-08-17 21:57:22 +08:00)\n"
     ]
    }
   ],
   "source": [
    "v[:, 2].assign([0., 1.])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 38,
   "id": "spatial-sharp",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<tf.Variable 'UnreadVariable' shape=(2, 3) dtype=float32, numpy=\n",
       "array([[100.,  42.,   0.],\n",
       "       [  8.,  10., 200.]], dtype=float32)>"
      ]
     },
     "execution_count": 38,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 10.4 ms (started: 2021-08-17 21:57:37 +08:00)\n"
     ]
    }
   ],
   "source": [
    "v.scatter_nd_update(indices=[[0, 0], [1, 2]], updates=[100., 200.])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "manufactured-catholic",
   "metadata": {},
   "source": [
    "> 在实践中，您很少需要手动创建变量，因为 Keras 提供了一个 add_weight() 方法，它将为您处理它，正如我们将看到的。 此外，模型参数通常会由优化器直接更新，因此您很少需要手动更新变量。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "marked-school",
   "metadata": {},
   "source": [
    "### 其他数据结构"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "strange-register",
   "metadata": {},
   "source": [
    "TensorFlow 支持其他几种数据结构，包括以下数据结构（请参阅笔记本或附录 F 中的“张量和运算”部分了解更多详细信息）：\n",
    "* 稀疏张量 (tf.SparseTensor)  \n",
    "有效地表示主要包含零的张量。 tf.sparse 包包含稀疏张量的操作。\n",
    "* 张量数组 (tf.TensorArray)  \n",
    "是张量列表。 默认情况下，它们具有固定大小，但可以选择设置为动态大小。 它们包含的所有张量必须具有相同的形状和数据类型。\n",
    "* 参差不齐的张量 (tf.RaggedTensor)  \n",
    "表示张量列表的静态列表，其中每个张量具有相同的形状和数据类型。 tf.ragged 包包含对不规则张量的操作。\n",
    "* 字符串张量  \n",
    "是 tf.string 类型的常规张量。 这些代表字节字符串，而不是 Unicode 字符串，因此如果您使用 Unicode 字符串（例如，像“café”这样的常规 Python 3 字符串）创建字符串张量，那么它将自动编码为 UTF-8（例如，`b\"caf \\xc3\\xa9\"`)。 或者，您可以使用 tf.int32 类型的张量表示 Unicode 字符串，其中每个项目表示一个 Unicode 代码点（例如，[99, 97, 102, 233]）。 tf.strings 包（带有 s）包含字节字符串和 Unicode 字符串的操作（以及将一个转换为另一个）。 重要的是要注意 tf.string 是原子的，这意味着它的长度不会出现在张量的形状中。 一旦将其转换为 Unicode 张量（即，类型为 tf.int32 的张量保存 Unicode 代码点），长度就会出现在形状中。\n",
    "* 集合  \n",
    "表示为常规张量（或稀疏张量）。 例如，tf.constant([[1, 2], [3, 4]]) 表示两个集合 {1, 2} 和 {3, 4}。 更一般地，每个集合由张量最后一个轴上的向量表示。 您可以使用 tf.sets 包中的操作来操作集合。\n",
    "* 队列  \n",
    "跨多个步骤存储张量。 TensorFlow 提供了多种队列：简单的先进先出 (FIFO) 队列 (FIFOQueue)、可以对某些项目进行优先级排序 (PriorityQueue)、对它们的项目进行混洗 (RandomShuffleQueue) 以及通过填充来批量处理不同形状的项目 (PaddingFIFOQueue)。 这些类都在 tf.queue 包中。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "exciting-immune",
   "metadata": {},
   "source": [
    "有了张量、操作、变量和各种数据结构供您使用，您现在可以自定义模型和训练算法了！"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "official-illinois",
   "metadata": {},
   "source": [
    "## 自定义模型和训练算法"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "martial-converter",
   "metadata": {},
   "source": [
    "让我们从创建自定义损失函数开始，这是一个简单而常见的用例。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "considerable-amplifier",
   "metadata": {},
   "source": [
    "### 自定义损失函数"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "wrong-myanmar",
   "metadata": {},
   "source": [
    "假设你想训练一个回归模型，但你的训练集有点嘈杂。当然，您首先尝试通过删除或修复异常值来清理数据集，但结果证明这是不够的；数据集仍然嘈杂。你应该使用哪个损失函数？均方误差可能会过多地惩罚大误差并导致您的模型不精确。平均绝对误差不会对异常值造成太大的惩罚，但训练可能需要一段时间才能收敛，并且训练后的模型可能不是很精确。这可能是使用 Huber 损失（在第 10 章中介绍）而不是旧的 MSE 的好时机。 Huber loss 目前不是官方 Keras API 的一部分，但它在 tf.keras 中可用（只需使用 keras.losses.Huber 类的实例）。但是让我们假设它不存在：实现它就像馅饼一样简单！只需创建一个将标签和预测作为参数的函数，并使用 TensorFlow 操作来计算每个实例的损失："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 39,
   "id": "massive-defendant",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 965 µs (started: 2021-08-17 22:06:42 +08:00)\n"
     ]
    }
   ],
   "source": [
    "def huber_fn(y_true, y_pred):\n",
    "    error = y_true - y_pred\n",
    "    is_small_error = tf.abs(error) < 1\n",
    "    squared_loss = tf.square(error) / 2\n",
    "    linear_loss = tf.abs(error) - 0.5\n",
    "    return tf.where(is_small_error, squared_loss, linear_loss)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "pleased-traffic",
   "metadata": {},
   "source": [
    "> 为了获得更好的性能，您应该使用矢量化实现，如本例所示。 此外，如果您想从 TensorFlow 的图形功能中受益，您应该只使用 TensorFlow 操作。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "preliminary-bermuda",
   "metadata": {},
   "source": [
    "最好返回一个包含每个实例一个损失的张量，而不是返回平均损失。 这样，Keras 可以在需要时应用类权重或样本权重（参见第 10 章）。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "delayed-bobby",
   "metadata": {},
   "source": [
    "现在您可以在编译 Keras 模型时使用此损失，然后训练您的模型："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "realistic-fight",
   "metadata": {},
   "outputs": [],
   "source": [
    "model.compile(loss=huber_fn, optimizer=\"nadam\")\n",
    "model.fit(X_train, y_train, [...])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "robust-reward",
   "metadata": {},
   "source": [
    "就是这样！ 对于训练期间的每个批次，Keras 将调用 huber_fn() 函数来计算损失并使用它来执行梯度下降步骤。 此外，它将跟踪自 epoch 开始以来的总损失，并显示平均损失。\n",
    "\n",
    "但是当您保存模型时，这种自定义损失会发生什么？"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "quantitative-announcement",
   "metadata": {},
   "source": [
    "### 保存和加载包含自定义组件的模型"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "utility-amino",
   "metadata": {},
   "source": [
    "保存包含自定义损失函数的模型效果很好，因为 Keras 保存了函数的名称。 无论何时加载它，您都需要提供一个将函数名称映射到实际函数的字典。 更一般地，当您加载包含自定义对象的模型时，您需要将名称映射到对象："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "western-personality",
   "metadata": {},
   "outputs": [],
   "source": [
    "model = keras.models.load_model(\"my_model_with_a_custom_loss.h5\", \n",
    "                                custom_objects={\"huber_fn\": huber_fn})"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "patent-change",
   "metadata": {},
   "source": [
    "在当前的实现中，–1 和 1 之间的任何误差都被认为是“小”的。 但是如果你想要一个不同的阈值怎么办？ 一种解决方案是创建一个函数来创建配置的损失函数："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "continuing-labor",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 类似修饰函数\n",
    "def create_huber(threshold=1.0):\n",
    "    def huber_fn(y_true, y_pred):\n",
    "        error = y_true - y_pred\n",
    "        is_small_error = tf.abs(error) < threshold\n",
    "        squared_loss = tf.square(error) / 2\n",
    "        linear_loss = threshold * tf.abs(error) - threshold**2 / 2\n",
    "        return tf.where(is_small_error, squared_loss, linear_loss)\n",
    "    return huber_fn\n",
    "model.compile(loss=create_huber(2.0), optimizer=\"nadam\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fifty-consultation",
   "metadata": {},
   "source": [
    "不幸的是，当您保存模型时，阈值不会被保存。 这意味着您必须在加载模型时指定阈值（请注意，要使用的名称是“huber_fn”，这是您提供给 Keras 的函数的名称，而不是创建它的函数的名称）："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "affiliated-church",
   "metadata": {},
   "outputs": [],
   "source": [
    "model = keras.models.load_model(\"my_model_with_a_custom_loss_threshold_2.h5\",\n",
    "                                custom_objects={\"huber_fn\": create_huber(2.0)})"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "inappropriate-dietary",
   "metadata": {},
   "source": [
    "您可以通过创建 keras.losses.Loss 类的子类，然后实现其 get_config() 方法来解决此问题："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "conservative-state",
   "metadata": {},
   "outputs": [],
   "source": [
    "class HuberLoss(keras.losses.Loss):\n",
    "    def __init__(self, threshold=1.0, **kwargs):\n",
    "        self.threshold = threshold\n",
    "        super().__init__(**kwargs)\n",
    "    def call(self, y_true, y_pred):\n",
    "        error = y_true - y_pred\n",
    "        is_small_error = tf.abs(error) < self.threshold\n",
    "        squared_loss = tf.square(error) / 2\n",
    "        linear_loss = self.threshold * tf.abs(error) - self.threshold**2 / 2\n",
    "        return tf.where(is_small_error, squared_loss, linear_loss)\n",
    "    def get_config(self):\n",
    "        base_config = super().get_config()\n",
    "        return {**base_config, \"threshold\": self.threshold}"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "occasional-genealogy",
   "metadata": {},
   "source": [
    "Keras API 目前仅指定如何使用子类化来定义层、模型、回调和正则化器。 如果您使用子类化构建其他组件（例如损失、度量、初始值设定项或约束），则它们可能无法移植到其他 Keras 实现。 Keras API 可能会更新以指定所有这些组件的子类化。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "spread-diploma",
   "metadata": {},
   "source": [
    "让我们来看看这段代码：\n",
    "* 构造函数接受`**kwargs` 并将它们传递给父构造函数，后者处理标准超参数：损失的名称和用于聚合单个实例损失的减少算法。 默认情况下，它是“sum_over_batch_size”，这意味着损失将是实例损失的总和，由样本权重加权，如果有的话，除以批量大小（不是权重之和，所以这不是 加权平均值）。 其他可能的值是“sum”和“none”。 \n",
    "* `call()` 方法获取标签和预测，计算所有实例损失，并返回它们。\n",
    "* `get_config()` 方法返回一个字典，将每个超参数名称映射到其值。 它首先调用父类的 get_config() 方法，然后将新的超参数添加到这个字典中（注意 Python 3.5 中添加了方便的 {**x} 语法）。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "expected-telescope",
   "metadata": {},
   "source": [
    "然后，您可以在编译模型时使用此类的任何实例："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "breathing-provision",
   "metadata": {},
   "outputs": [],
   "source": [
    "model.compile(loss=HuberLoss(2.), optimizer=\"nadam\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "religious-nevada",
   "metadata": {},
   "source": [
    "保存模型时，阈值也会随之保存； 当你加载模型时，你只需要将类名映射到类本身："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "transparent-wilderness",
   "metadata": {},
   "outputs": [],
   "source": [
    "model = keras.models.load_model(\"my_model_with_a_custom_loss_class.h5\",\n",
    "                                custom_objects={\"HuberLoss\": HuberLoss})"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "russian-product",
   "metadata": {},
   "source": [
    "当您保存模型时，Keras 会调用损失实例的 `get_config()` 方法并将配置以 JSON 格式保存在 HDF5 文件中。 当您加载模型时，它会调用 HuberLoss 类上的 `from_config()` 类方法：此方法由基类 (Loss) 实现并创建该类的实例，将 `**config` 传递给构造函数。\n",
    "\n",
    "这就是损失！ 这并不太难，是吗？ 自定义激活函数、初始化器、正则化器和约束同样简单。 现在让我们看看这些。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "located-hopkins",
   "metadata": {},
   "source": [
    "### 自定义激活函数、初始化器、正则化器和约束"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "noted-surveillance",
   "metadata": {},
   "source": [
    "大多数 Keras 功能，例如损失、正则化器、约束、初始化器、度量、激活函数、层，甚至完整模型，都可以以非常相同的方式进行定制。 大多数情况下，您只需要编写一个具有适当输入和输出的简单函数。 以下是自定义激活函数（相当于 `keras.activations.softplus()` 或 `tf.nn.softplus()`）、自定义 Glorot 初始值设定项（相当于 `keras.initializers.glorot_normal()`）、自定义 ℓ1 正则化器（相当于 到 `keras.regularizers.l1(0.01)`)，以及确保权重均为正的自定义约束（相当于 `keras.constraints.nonneg()` 或 `tf.nn.relu()`）："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 61,
   "id": "laughing-emission",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 1.58 ms (started: 2021-08-17 23:49:57 +08:00)\n"
     ]
    }
   ],
   "source": [
    "def my_softplus(z): # return value is just tf.nn.softplus(z)\n",
    "    return tf.math.log(tf.exp(z) + 1.0)\n",
    "\n",
    "def my_glorot_initializer(shape, dtype=tf.float32):\n",
    "    stddev = tf.sqrt(2. / (shape[0] + shape[1]))\n",
    "    return tf.random.normal(shape, stddev=stddev, dtype=dtype)\n",
    "\n",
    "def my_l1_regularizer(weights):\n",
    "    return tf.reduce_sum(tf.abs(0.01 * weights))\n",
    "\n",
    "def my_positive_weights(weights): # return value is just tf.nn.relu(weights)\n",
    "    return tf.where(weights < 0., tf.zeros_like(weights), weights)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "nasty-inflation",
   "metadata": {},
   "source": [
    "如您所见，参数取决于自定义函数的类型。 然后就可以正常使用这些自定义函数了； 例如："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "powered-copper",
   "metadata": {},
   "outputs": [],
   "source": [
    "layer = keras.layers.Dense(30, activation=my_softplus,\n",
    "                           kernel_initializer=my_glorot_initializer,\n",
    "                           kernel_regularizer=my_l1_regularizer,\n",
    "                           kernel_constraint=my_positive_weights)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "touched-fountain",
   "metadata": {},
   "source": [
    "激活函数将应用于此 Dense 层的输出，其结果将传递到下一层。 层的权重将使用初始化程序返回的值进行初始化。 在每个训练步骤中，权重将传递给正则化函数以计算正则化损失，将其添加到主要损失中以获得用于训练的最终损失。 最后，在每个训练步骤之后都会调用约束函数，并且层的权重将被约束权重替换。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "capable-challenge",
   "metadata": {},
   "source": [
    "如果函数具有需要与模型一起保存的超参数，那么您将需要子类化适当的类，例如 keras.regularizers.Regularizer、keras.constraints.Constraint、keras.initializers.Initializer 或 keras.layers。 层（对于任何层，包括激活函数）。 就像我们对自定义损失所做的一样，这里有一个简单的 ℓ1 正则化类，可以保存它的因子\n",
    "超参数（这次我们不需要调用父构造函数或 get_config() 方法，因为它们不是由父类定义的）："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "surrounded-nutrition",
   "metadata": {},
   "outputs": [],
   "source": [
    "class MyL1Regularizer(keras.regularizers.Regularizer):\n",
    "    def __init__(self, factor):\n",
    "        self.factor = factor\n",
    "    def __call__(self, weights):\n",
    "        return tf.reduce_sum(tf.abs(self.factor * weights))\n",
    "    def get_config(self):\n",
    "        return {\"factor\": self.factor}"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "classical-magic",
   "metadata": {},
   "source": [
    "请注意，您必须为损失、层（包括激活函数）和模型实现 `call()` 方法，或者为正则化器、初始化器和约束实现 `__call__()` 方法。 对于指标，情况有点不同，我们现在将看到。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "parallel-measurement",
   "metadata": {},
   "source": [
    "### 自定义指标"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "verified-corrections",
   "metadata": {},
   "source": [
    "损失和度量在概念上不是一回事：梯度下降使用损失（例如，交叉熵）来训练模型，因此它们必须是可微的（至少在评估它们的地方），并且它们的梯度不应该在任何地方都为 0。 另外，如果它们不容易被人类解释，那也没关系。 相比之下，指标（例如，准确性）用于评估模型：它们必须更容易解释，并且它们可以是不可微的或在任何地方都有 0 梯度。 也就是说，在大多数情况下，定义自定义度量函数与定义自定义损失函数完全相同。 事实上，我们甚至可以使用我们之前创建的 Huber 损失函数作为度量； 它会工作得很好（并且持久性也会以相同的方式工作，在这种情况下只保存函数的名称，“huber_fn”）："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "surprised-trash",
   "metadata": {},
   "outputs": [],
   "source": [
    "model.compile(loss=\"mse\", optimizer=\"nadam\", metrics=[create_huber(2.0)])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "stylish-subsection",
   "metadata": {},
   "source": [
    "对于训练期间的每个批次，Keras 将计算该指标并跟踪自 epoch 开始以来的平均值。大多数时候，这正是您想要的。但不总是！例如，考虑一个二元分类器的精度。正如我们在第 3 章中看到的，精度是真阳性的数量除以阳性预测的数量（包括真阳性和假阳性）。假设模型在第一批中做出了五个正面预测，其中四个是正确的：准确率为 80%。然后假设模型在第二批中做出了三个积极的预测，但它们都是错误的：第二批的精度为 0%。如果你只计算这两个精度的平均值，你会得到 40%。但是等一下——这不是模型在这两个批次上的精度！实际上，在八个正预测 (5 + 3) 中总共有四个真阳性 (4 + 0)，因此总体精度是 50%，而不是 40%。我们需要的是一个对象，它可以跟踪真阳性的数量和误报的数量，并且可以在需要时计算它们的比率。这正是 keras.metrics.Precision 类所做的："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 40,
   "id": "indirect-survey",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<tf.Tensor: shape=(), dtype=float32, numpy=0.8>"
      ]
     },
     "execution_count": 40,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 186 ms (started: 2021-08-17 22:37:36 +08:00)\n"
     ]
    }
   ],
   "source": [
    "precision = keras.metrics.Precision()\n",
    "precision([0, 1, 1, 1, 0, 1, 0, 1], [1, 1, 0, 1, 0, 1, 0, 1])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 41,
   "id": "heard-praise",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<tf.Tensor: shape=(), dtype=float32, numpy=0.5>"
      ]
     },
     "execution_count": 41,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 17.7 ms (started: 2021-08-17 22:37:48 +08:00)\n"
     ]
    }
   ],
   "source": [
    "precision([0, 1, 0, 0, 1, 0, 1, 1], [1, 0, 1, 1, 0, 0, 0, 0])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "golden-stevens",
   "metadata": {},
   "source": [
    "在这个例子中，我们创建了一个 Precision 对象，然后我们像函数一样使用它，将第一批的标签和预测传递给它，然后第二批（注意我们也可以传递样本权重）。 我们使用了与刚才讨论的示例中相同数量的真假阳性。 第一批后，返回80%的精度； 然后在第二批之后，它返回 50%（这是目前的整体精度，而不是第二批的精度）。 这被称为流指标（或有状态指标），因为它是逐批逐渐更新的。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "stunning-stevens",
   "metadata": {},
   "source": [
    "在任何时候，我们都可以调用 result() 方法来获取度量的当前值。 我们还可以使用 variables 属性查看它的变量（跟踪真假阳性的数量），我们可以使用 reset_states() 方法重置这些变量："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 42,
   "id": "voluntary-eligibility",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<tf.Tensor: shape=(), dtype=float32, numpy=0.5>"
      ]
     },
     "execution_count": 42,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 9.72 ms (started: 2021-08-17 22:39:16 +08:00)\n"
     ]
    }
   ],
   "source": [
    "precision.result()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 43,
   "id": "satisfactory-brake",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[<tf.Variable 'true_positives:0' shape=(1,) dtype=float32, numpy=array([4.], dtype=float32)>,\n",
       " <tf.Variable 'false_positives:0' shape=(1,) dtype=float32, numpy=array([4.], dtype=float32)>]"
      ]
     },
     "execution_count": 43,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 4.71 ms (started: 2021-08-17 22:39:24 +08:00)\n"
     ]
    }
   ],
   "source": [
    "precision.variables"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 44,
   "id": "sorted-geneva",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 7.91 ms (started: 2021-08-17 22:39:47 +08:00)\n"
     ]
    }
   ],
   "source": [
    "precision.reset_states() # both variables get reset to 0.0"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "partial-strategy",
   "metadata": {},
   "source": [
    "如果您需要创建这样的流指标，请创建 keras.metrics.Metric 类的子类。 这是一个简单的例子，它跟踪总的 Huber 损失和到目前为止看到的实例数。 当询问结果时，它返回比率，即平均 Huber 损失："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "substantial-invention",
   "metadata": {},
   "outputs": [],
   "source": [
    "class HuberMetric(keras.metrics.Metric):\n",
    "    def __init__(self, threshold=1.0, **kwargs):\n",
    "        super().__init__(**kwargs) # handles base args (e.g., dtype)\n",
    "        self.threshold = threshold\n",
    "        self.huber_fn = create_huber(threshold)\n",
    "        self.total = self.add_weight(\"total\", initializer=\"zeros\")\n",
    "        self.count = self.add_weight(\"count\", initializer=\"zeros\")\n",
    "    def update_state(self, y_true, y_pred, sample_weight=None):\n",
    "        metric = self.huber_fn(y_true, y_pred)\n",
    "        self.total.assign_add(tf.reduce_sum(metric))\n",
    "        self.count.assign_add(tf.cast(tf.size(y_true), tf.float32))\n",
    "    def result(self):\n",
    "        return self.total / self.count\n",
    "    def get_config(self):\n",
    "        base_config = super().get_config()\n",
    "        return {**base_config, \"threshold\": self.threshold}"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "olympic-venice",
   "metadata": {},
   "source": [
    "让我们来看看这段代码：\n",
    "* 构造函数使用 add_weight() 方法来创建在多个批次中跟踪指标状态所需的变量——在本例中，是所有 Huber 损失的总和（总计）和目前看到的实例数（计数）。 如果您愿意，您可以手动创建变量。 Keras 跟踪任何设置为属性的 tf.Variable（更一般地，任何“可跟踪”对象，例如层或模型）。\n",
    "* 当您将此类的实例用作函数时（就像我们对 Precision 对象所做的那样），将调用 update_state() 方法。 给定一批的标签和预测（以及样本权重，但在这种情况下我们忽略它们），它会更新变量。\n",
    "* result() 方法计算并返回最终结果，在这种情况下是所有实例的平均 Huber 指标。 当您将指标用作函数时，首先调用 update_state() 方法，然后调用 result() 方法，并返回其输出。\n",
    "* 我们还实现了 get_config() 方法以确保阈值与模型一起保存。\n",
    "* reset_states() 方法的默认实现将所有变量重置为0.0（但您可以根据需要覆盖它）。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "outer-graphic",
   "metadata": {},
   "source": [
    "> Keras 将无缝处理变量持久性； 无需任何操作。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "desperate-independence",
   "metadata": {},
   "source": [
    "当你使用一个简单的函数定义一个度量时，Keras 会自动为每个批次调用它，并且它会跟踪每个时期的平均值，就像我们手动做的那样。 所以我们 HuberMetric 类的唯一好处是阈值将被保存。 但是，当然，某些指标（例如精度）不能简单地对批次进行平均：在这些情况下，除了实施流指标之外别无选择。\n",
    "\n",
    "现在我们已经构建了一个流指标，构建一个自定义层就像在公园里散步一样！"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "herbal-pavilion",
   "metadata": {},
   "source": [
    "### 自定义层"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "careful-mount",
   "metadata": {},
   "source": [
    "您可能偶尔想要构建一个架构，其中包含 TensorFlow 不提供默认实现的奇异层。 在这种情况下，您需要创建一个自定义层。 或者您可能只是想构建一个非常重复的架构，包含重复多次的相同层块，并且将每个层块视为单个层会很方便。 例如，如果模型是 A、B、C、A、B、C、A、B、C 层的序列，那么您可能需要定义一个包含 A、B、C 层的自定义层 D，因此您的模型 然后就是 D、D、D。让我们看看如何构建自定义层。\n",
    "\n",
    "首先，有些层没有权重，例如 keras.layers.Flatten 或 keras.layers.ReLU。 如果您想创建一个没有任何权重的自定义层，最简单的选择是编写一个函数并将其包装在 keras.layers.Lambda 层中。 例如，以下层将指数函数应用于其输入："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "rural-institute",
   "metadata": {},
   "outputs": [],
   "source": [
    "exponential_layer = keras.layers.Lambda(lambda x: tf.exp(x))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "typical-oxygen",
   "metadata": {},
   "source": [
    "然后可以像任何其他层一样使用这个自定义层，使用 Sequential API、Functional API 或 Subclassing API。 您也可以将其用作激活函数（或者您可以使用 activation=tf.exp、activation=keras.activations.exponential 或简单地 activation=\"exponential\"）。 当要预测的值具有非常不同的尺度（例如，0.001、10.、1,000.）时，指数层有时用于回归模型的输出层。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "comprehensive-wonder",
   "metadata": {},
   "source": [
    "正如您现在可能已经猜到的那样，要构建自定义有状态层（即具有权重的层），您需要创建 keras.layers.Layer 类的子类。 例如，以下类实现了 Dense 层的简化版本："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "buried-concentrate",
   "metadata": {},
   "outputs": [],
   "source": [
    "class MyDense(keras.layers.Layer):\n",
    "    def __init__(self, units, activation=None, **kwargs):\n",
    "        super().__init__(**kwargs)\n",
    "        self.units = units\n",
    "        self.activation = keras.activations.get(activation)\n",
    "        \n",
    "    def build(self, batch_input_shape):\n",
    "        self.kernel = self.add_weight(name=\"kernel\", \n",
    "                                      shape=[batch_input_shape[-1], self.units],\n",
    "                                      initializer=\"glorot_normal\")\n",
    "        self.bias = self.add_weight(name=\"bias\", \n",
    "                                    shape=[self.units], \n",
    "                                    initializer=\"zeros\")\n",
    "        super().build(batch_input_shape) # must be at the end\n",
    "        \n",
    "    def call(self, X):\n",
    "        return self.activation(X @ self.kernel + self.bias)\n",
    "    \n",
    "    def compute_output_shape(self, batch_input_shape):\n",
    "        return tf.TensorShape(batch_input_shape.as_list()[:-1] + [self.units])\n",
    "    \n",
    "    def get_config(self):\n",
    "        base_config = super().get_config()\n",
    "        return {**base_config, \n",
    "                \"units\": self.units,\n",
    "                \"activation\": keras.activations.serialize(self.activation)}"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "integral-stranger",
   "metadata": {},
   "source": [
    "让我们来看看这段代码：\n",
    "* 构造函数将`所有超参数`作为参数（在本例中为单位和激活），重要的是它还采用 `**kwargs` 参数。 它调用父构造函数，将 `kwargs` 传递给它：它负责处理标准参数，例如 `input_shape`、`trainable` 和 `name`。 然后它将超参数保存为属性，使用 `keras.activations.get()` 函数将激活参数转换为适当的激活函数（它接受函数、标准字符串，如“`relu`”或“`selu`”，或者只是 `None`）。\n",
    "* `build()` 方法的作用是通过为每个权重调用 `add_weight()` 方法来创建层的变量。 `build()` 方法在第一次使用层时被调用。 那时，Keras 将知道该层输入的形状，并将其传递给 `build()` 方法，这通常是创建一些权重所必需的。 例如，为了创建连接权重矩阵（即“内核”），我们需要知道前一层神经元的数量：这对应于输入的最后一个维度的大小。 在 `build()` 方法的末尾（并且仅在末尾），您必须调用父级的 `build()` 方法：这告诉 Keras 该层已构建（它只是设置 `self.built=True`）。\n",
    "* `call()` 方法执行所需的操作。 在这种情况下，我们计算输入 X 和层内核的矩阵乘法，我们添加偏置向量，并将激活函数应用于结果，这给了我们层的输出。\n",
    "* `compute_output_shape()` 方法只返回该层输出的形状。 在这种情况下，它与输入的形状相同，只是最后一个维度被替换为层中的神经元数量。 请注意，在 tf.keras 中，形状是 tf.TensorShape 类的实例，您可以使用 `as_list()` 将其转换为 Python 列表。\n",
    "* `get_config()` 方法就像在以前的自定义类中一样。 请注意，我们通过调用 `keras.activations.serialize()` 保存了激活函数的完整配置。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "final-minority",
   "metadata": {},
   "source": [
    "您现在可以像使用任何其他图层一样使用 MyDense 图层！"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "multiple-leather",
   "metadata": {},
   "source": [
    "您通常可以省略 compute_output_shape() 方法，因为 tf.keras 会自动推断输出形状，除非图层是动态的（我们很快就会看到）。 在其他 Keras 实现中，此方法要么是必需的，要么其默认实现假定输出形状与输入形状相同。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "amino-fireplace",
   "metadata": {},
   "source": [
    "要创建具有多个输入的层（例如，Concatenate），call() 方法的参数应该是包含所有输入的元组，同样，compute_output_shape() 方法的参数应该是包含每个输入的批处理形状的元组。 要创建具有多个输出的层， call() 方法应返回输出列表，并且 compute_output_shape() 应返回批量输出形状列表（每个输出一个）。 例如，以下玩具层采用两个输入并返回三个输出："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "intended-popularity",
   "metadata": {},
   "outputs": [],
   "source": [
    "class MyMultiLayer(keras.layers.Layer):\n",
    "    def call(self, X):\n",
    "        X1, X2 = X\n",
    "        return [X1 + X2, X1 * X2, X1 / X2]\n",
    "    def compute_output_shape(self, batch_input_shape):\n",
    "        b1, b2 = batch_input_shape\n",
    "        return [b1, b1, b1] # should probably handle broadcasting rules"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "headed-drawing",
   "metadata": {},
   "source": [
    "这一层现在可以像任何其他层一样使用，但当然只使用函数和子类 API，而不是顺序 API（它只接受具有一个输入和一个输出的层）。\n",
    "\n",
    "如果您的层在训练和测试期间需要具有不同的行为（例如，如果它使用 Dropout 或 BatchNormalization 层），那么您必须向 call() 方法添加一个训练参数，并使用该参数来决定要做什么。 例如，让我们创建一个在训练期间添加高斯噪声的层（用于正则化）但在测试期间不做任何事情（Keras 有一个层可以做同样的事情，keras.layers.GaussianNoise）："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "colonial-forward",
   "metadata": {},
   "outputs": [],
   "source": [
    "class MyGaussianNoise(keras.layers.Layer):\n",
    "    def __init__(self, stddev, **kwargs):\n",
    "        super().__init__(**kwargs)\n",
    "        self.stddev = stddev\n",
    "        \n",
    "    def call(self, X, training=None):\n",
    "        if training:\n",
    "            noise = tf.random.normal(tf.shape(X), stddev=self.stddev)\n",
    "            return X + noise\n",
    "        else:\n",
    "            return X\n",
    "    \n",
    "    def compute_output_shape(self, batch_input_shape):\n",
    "        return batch_input_shape"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "occasional-texas",
   "metadata": {},
   "source": [
    "有了它，您现在可以构建您需要的任何自定义层！ 现在让我们创建自定义模型。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "floating-manner",
   "metadata": {},
   "source": [
    "### 定制模型"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "agricultural-hardware",
   "metadata": {},
   "source": [
    "在第 10 章讨论子类化 API 时，我们已经看过创建自定义模型类。很简单：子类化 keras.Model 类，在构造函数中创建层和变量，并实现 call() 方法来做任何你想做的事 模型来做。 假设您要构建图 12-3 所示的模型。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "commercial-juvenile",
   "metadata": {},
   "source": [
    "![](https://tva1.sinaimg.cn/large/008i3skNgy1gtk6wbot9nj60ws0jcgn702.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "controlled-charge",
   "metadata": {},
   "source": [
    "输入通过第一个密集层，然后通过由两个密集层和加法操作组成的残差块（正如我们将在第 14 章中看到的，残差块将其输入添加到其输出），然后通过相同的残差块三 更多次，然后通过第二个残差块，最终结果通过一个密集的输出层。 请注意，这个模型没有多大意义； 这只是一个示例，说明您可以轻松构建所需的任何类型的模型，即使是包含循环和跳过连接的模型。 为了实现这个模型，最好先创建一个 ResidualBlock 层，因为我们将创建几个相同的块（我们可能想在另一个模型中重用它）："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "moderate-shepherd",
   "metadata": {},
   "outputs": [],
   "source": [
    "class ResidualBlock(keras.layers.Layer):\n",
    "    def __init__(self, n_layers, n_neurons, **kwargs):\n",
    "        super().__init__(**kwargs)\n",
    "        self.hidden = [keras.layers.Dense(n_neurons, \n",
    "                                          activation=\"elu\",\n",
    "                                          kernel_initializer=\"he_normal\")\n",
    "                       for _ in range(n_layers)]\n",
    "    \n",
    "    def call(self, inputs):\n",
    "        Z = inputs\n",
    "        for layer in self.hidden: \n",
    "            Z = layer(Z)\n",
    "        return inputs + Z"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "historical-nutrition",
   "metadata": {},
   "source": [
    "这个层有点特殊，因为它包含其他层。 这由 Keras 透明处理：它会自动检测隐藏属性包含可跟踪对象（在本例中为层），因此它们的变量会自动添加到该层的变量列表中。 本课程的其余部分是不言自明的。 接下来，让我们使用 Subclassing API 来定义模型本身："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "laden-planner",
   "metadata": {},
   "outputs": [],
   "source": [
    "class ResidualRegressor(keras.Model):\n",
    "    def __init__(self, output_dim, **kwargs):\n",
    "        super().__init__(**kwargs)\n",
    "        self.hidden1 = keras.layers.Dense(30, \n",
    "                                          activation=\"elu\",\n",
    "                                          kernel_initializer=\"he_normal\")\n",
    "        self.block1 = ResidualBlock(2, 30)\n",
    "        self.block2 = ResidualBlock(2, 30)\n",
    "        self.out = keras.layers.Dense(output_dim)\n",
    "    \n",
    "    def call(self, inputs):\n",
    "        Z = self.hidden1(inputs)\n",
    "        for _ in range(1 + 3):\n",
    "            Z = self.block1(Z) \n",
    "            Z = self.block2(Z)\n",
    "        return self.out(Z)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "selected-brown",
   "metadata": {},
   "source": [
    "我们在构造函数中创建层并在 call() 方法中使用它们。 然后可以像使用任何其他模型一样使用该模型（编译、拟合、评估并使用它进行预测）。 如果您还希望能够使用 save() 方法保存模型并使用 keras.models.load_model() 函数加载它，您必须在 ResidualBlock 类中实现 get_config() 方法（正如我们之前所做的那样） 和 ResidualRegressor 类。 或者，您可以使用 save_weights() 和 load_weights() 方法保存和加载权重。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "plain-flash",
   "metadata": {},
   "source": [
    "Model 类是 Layer 类的子类，因此可以像层一样定义和使用模型。 但是模型有一些额外的功能，当然包括它的 compile()、fit()、evaluate() 和 predict() 方法（和一些变体），加上 get_layers() 方法（它可以返回任何模型的 按名称或索引分层）和 save() 方法（并支持 keras.models.load_model() 和 keras.models.clone_model()）。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "collect-feeling",
   "metadata": {},
   "source": [
    "> 如果模型提供比层更多的功能，为什么不将每一层定义为一个模型？ 好吧，从技术上讲，您可以，但通常更清晰地将模型的内部组件（即层或可重用的层块）与模型本身（即您将训练的对象）区分开来。 前者应该继承Layer 类，而后者应该继承Model 类。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "handed-discipline",
   "metadata": {},
   "source": [
    "有了它，您可以使用 Sequential API、Functional API、Subclassing API 甚至这些的组合，自然而简洁地构建几乎任何在论文中找到的模型。 “几乎”任何型号？ 是的，我们仍然需要考虑一些事情：首先，如何基于模型内部定义损失或指标，其次，如何构建自定义训练循环。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "preceding-blair",
   "metadata": {},
   "source": [
    "### 基于模型内部的损失和度量"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "played-automation",
   "metadata": {},
   "source": [
    "我们之前定义的自定义损失和指标都基于标签和预测（以及可选的样本权重）。有时您希望根据模型的其他部分（例如其隐藏层的权重或激活）来定义损失。这对于正则化目的或监视模型的某些内部方面可能很有用。\n",
    "\n",
    "要基于模型内部定义自定义损失，请根据您想要的模型的任何部分计算它，然后将结果传递给 add_loss() 方法。 例如，让我们构建一个由五个隐藏的堆栈组成的自定义回归 MLP 模型层加上一个输出层。此自定义模型还将在上隐藏层之上有一个辅助输出。与这个辅助输出相关的损失将被称为重建损失（见第 17 章）：它是重建和输入之间的均方差。通过将此重建损失添加到主要损失中，我们将鼓励模型通过隐藏层保留尽可能多的信息——即使是对回归任务本身没有直接用处的信息。在实践中，这种损失有时会提高泛化能力（这是一种正则化损失）。这是具有自定义重建损失的此自定义模型的代码："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "african-fossil",
   "metadata": {},
   "outputs": [],
   "source": [
    "class ReconstructingRegressor(keras.Model):\n",
    "    def __init__(self, output_dim, **kwargs):\n",
    "        super().__init__(**kwargs)\n",
    "        self.hidden = [keras.layers.Dense(30, \n",
    "                                          activation=\"selu\",\n",
    "                                          kernel_initializer=\"lecun_normal\")\n",
    "                       for _ in range(5)]\n",
    "        self.out = keras.layers.Dense(output_dim)\n",
    "    \n",
    "    def build(self, batch_input_shape):\n",
    "        n_inputs = batch_input_shape[-1]\n",
    "        self.reconstruct = keras.layers.Dense(n_inputs)\n",
    "        super().build(batch_input_shape)\n",
    "    \n",
    "    def call(self, inputs):\n",
    "        Z = inputs\n",
    "        for layer in self.hidden: \n",
    "            Z = layer(Z)\n",
    "        reconstruction = self.reconstruct(Z)\n",
    "        recon_loss = tf.reduce_mean(tf.square(reconstruction - inputs))\n",
    "        self.add_loss(0.05 * recon_loss)\n",
    "        return self.out(Z)s"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "thick-timer",
   "metadata": {},
   "source": [
    "让我们来看看这段代码：\n",
    "* 构造函数创建具有五个密集隐藏层和一个密集输出层的 DNN。\n",
    "* build() 方法创建一个额外的密集层，用于重建模型的输入。它必须在这里创建，因为它的单元数必须等于输入数，而这个数字在调用 build() 方法之前是未知的。\n",
    "* call() 方法通过所有五个隐藏层处理输入，然后将结果通过重建层，从而产生重建。\n",
    "* 然后 call() 方法计算重建损失（重建和输入之间的均方差），并使用 add_loss() 方法将其添加到模型的损失列表中。请注意，我们通过乘以 0.05（这是一个可以调整的超参数）来缩小重建损失。这确保了重建损失不会主导主要损失。\n",
    "* 最后，call() 方法将隐藏层的输出传递给输出层并返回其输出。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "speaking-syndication",
   "metadata": {},
   "source": [
    "同样，您可以通过以任何您想要的方式计算，添加基于模型内部的自定义度量，只要结果是度量对象的输出。 例如，您可以在构造函数中创建一个 keras.metrics.Mean 对象，然后在 call() 方法中调用它，将 recon_loss 传递给它，最后通过调用模型的 add_metric() 方法将其添加到模型中。 这样，当您训练模型时，Keras 将同时显示每个时期的平均损失（损失是主要损失加上重建损失的 0.05 倍）和每个时期的平均重建误差。 两者都会在训练期间下降："
   ]
  },
  {
   "cell_type": "markdown",
   "id": "satellite-transportation",
   "metadata": {},
   "source": [
    "![](https://tva1.sinaimg.cn/large/008i3skNgy1gtk7iqp2nrj61cu07igmg02.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "monetary-assets",
   "metadata": {},
   "source": [
    "在超过 99% 的情况下，我们迄今为止讨论的所有内容都足以实现您想要构建的任何模型，即使具有复杂的架构、损失和指标。 但是，在极少数情况下，您可能需要自定义训练循环本身。 在我们到达那里之前，我们需要看看如何在 TensorFlow 中自动计算梯度。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "pharmaceutical-muslim",
   "metadata": {},
   "source": [
    "### 使用 Autodiff 计算梯度"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "suspended-offense",
   "metadata": {},
   "source": [
    "要了解如何使用 autodiff（参见第 10 章和附录 D）自动计算梯度，让我们考虑一个简单的玩具函数："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 45,
   "id": "classical-north",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 1.39 ms (started: 2021-08-17 23:37:27 +08:00)\n"
     ]
    }
   ],
   "source": [
    "def f(w1, w2):\n",
    "    return 3 * w1 ** 2 + 2 * w1 * w2"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ranging-wesley",
   "metadata": {},
   "source": [
    "如果你懂微积分，你可以通过分析发现这个函数关于 w1 的偏导数是 6 * w1 + 2 * w2。 您还可以发现它关于 w2 的偏导数是 2 * w1。 例如，在 (w1, w2) = (5, 3) 点，这些偏导数分别等于 36 和 10，因此该点的梯度向量为 (36, 10)。 但是，如果这是一个神经网络，该函数将复杂得多，通常有数万个参数，并且手动分析偏导数将是一项几乎不可能完成的任务。 一种解决方案可能是通过测量调整相应参数时函数输出的变化来计算每个偏导数的近似值："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 46,
   "id": "competitive-wells",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 558 µs (started: 2021-08-17 23:37:31 +08:00)\n"
     ]
    }
   ],
   "source": [
    "w1, w2 = 5, 3"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 47,
   "id": "interesting-mounting",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 496 µs (started: 2021-08-17 23:37:37 +08:00)\n"
     ]
    }
   ],
   "source": [
    "eps = 1e-6"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 48,
   "id": "unable-angola",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "36.000003007075065"
      ]
     },
     "execution_count": 48,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 3.31 ms (started: 2021-08-17 23:37:45 +08:00)\n"
     ]
    }
   ],
   "source": [
    "(f(w1 + eps, w2) - f(w1, w2)) / eps"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 49,
   "id": "female-paradise",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "10.000000003174137"
      ]
     },
     "execution_count": 49,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 3.1 ms (started: 2021-08-17 23:37:54 +08:00)\n"
     ]
    }
   ],
   "source": [
    "(f(w1, w2 + eps) - f(w1, w2)) / eps"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "perfect-traveler",
   "metadata": {},
   "source": [
    "看起来差不多！ 这工作得很好并且很容易实现，但这只是一个近似值，重要的是你需要为每个参数至少调用一次 f() （不是两次，因为我们可以只计算一次 f(w1, w2) ）。 需要为每个参数至少调用一次 f() 使得这种方法对于大型神经网络来说难以处理。 因此，我们应该使用 `autodiff`。 TensorFlow 让这一切变得非常简单："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 51,
   "id": "suspected-above",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[<tf.Tensor: shape=(), dtype=float32, numpy=36.0>,\n",
       " <tf.Tensor: shape=(), dtype=float32, numpy=10.0>]"
      ]
     },
     "execution_count": 51,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 9.12 ms (started: 2021-08-17 23:39:20 +08:00)\n"
     ]
    }
   ],
   "source": [
    "w1, w2 = tf.Variable(5.), tf.Variable(3.)\n",
    "\n",
    "with tf.GradientTape() as tape: \n",
    "    z = f(w1, w2)\n",
    "    \n",
    "gradients = tape.gradient(z, [w1, w2])\n",
    "gradients"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "noble-contamination",
   "metadata": {},
   "source": [
    "我们首先定义两个变量 w1 和 w2，然后我们创建一个 tf.GradientTape 上下文，它会自动记录每个涉及变量的操作，最后我们要求这个磁带计算结果 z 关于这两个变量的梯度 [w1, w2]。 我们来看看 TensorFlow 计算的梯度："
   ]
  },
  {
   "cell_type": "markdown",
   "id": "determined-anatomy",
   "metadata": {},
   "source": [
    "完美的！ 不仅结果准确（精度只受浮点误差限制），而且gradient()方法只经过一次记录的计算（以相反的顺序），无论有多少变量，所以它是 令人难以置信的高效。 就像魔术一样！"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "recent-agreement",
   "metadata": {},
   "source": [
    "> 为了节省内存，只将严格的最小值放在 tf.GradientTape() 块中。 或者，通过在 tf.GradientTape() 块内创建一个 with tape.stop_recording() 块来暂停录制。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "removable-escape",
   "metadata": {},
   "source": [
    "调用它的 gradient() 方法后磁带会立即自动擦除，因此如果您尝试调用 gradient() 两次，则会出现异常："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "available-works",
   "metadata": {},
   "outputs": [],
   "source": [
    "with tf.GradientTape() as tape: \n",
    "    z = f(w1, w2)\n",
    "    \n",
    "dz_dw1 = tape.gradient(z, w1) # => tensor 36.0\n",
    "dz_dw2 = tape.gradient(z, w2) # RuntimeError!"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "surface-fundamentals",
   "metadata": {},
   "source": [
    "如果您需要多次调用gradient()，您必须使磁带持久化并在每次使用完毕后将其删除以释放资源："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 53,
   "id": "critical-sustainability",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 8.69 ms (started: 2021-08-17 23:43:46 +08:00)\n"
     ]
    }
   ],
   "source": [
    "with tf.GradientTape(persistent=True) as tape: \n",
    "    z = f(w1, w2)\n",
    "dz_dw1 = tape.gradient(z, w1) # => tensor 36.0\n",
    "dz_dw2 = tape.gradient(z, w2) # => tensor 10.0, works fine now!\n",
    "\n",
    "del tape"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "conventional-revision",
   "metadata": {},
   "source": [
    "默认情况下，磁带将仅跟踪涉及变量的操作，因此如果您尝试计算 z 与变量以外的任何其他内容相关的梯度，结果将为 None："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 55,
   "id": "satisfied-variation",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[None, None]"
      ]
     },
     "execution_count": 55,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 4.96 ms (started: 2021-08-17 23:44:31 +08:00)\n"
     ]
    }
   ],
   "source": [
    "c1, c2 = tf.constant(5.), tf.constant(3.)\n",
    "\n",
    "with tf.GradientTape() as tape: \n",
    "    z = f(c1, c2)\n",
    "    \n",
    "gradients = tape.gradient(z, [c1, c2]) # returns [None, None]\n",
    "gradients"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "young-analyst",
   "metadata": {},
   "source": [
    "但是，您可以强制磁带观看您喜欢的任何张量，以记录涉及它们的每个操作。 然后，您可以计算关于这些张量的梯度，就好像它们是变量一样："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 57,
   "id": "neither-exclusion",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[<tf.Tensor: shape=(), dtype=float32, numpy=36.0>,\n",
       " <tf.Tensor: shape=(), dtype=float32, numpy=10.0>]"
      ]
     },
     "execution_count": 57,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 6.38 ms (started: 2021-08-17 23:45:21 +08:00)\n"
     ]
    }
   ],
   "source": [
    "with tf.GradientTape() as tape:\n",
    "    tape.watch(c1)\n",
    "    tape.watch(c2) \n",
    "    z = f(c1, c2)\n",
    "    \n",
    "gradients = tape.gradient(z, [c1, c2]) # returns [tensor 36., tensor 10.]\n",
    "gradients"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "still-increase",
   "metadata": {},
   "source": [
    "这在某些情况下很有用，比如如果你想实现一个正则化损失，当输入变化很小时惩罚变化很大的激活：损失将基于输入的激活梯度。由于输入不是变量，您需要告诉磁带观看它们。\n",
    "\n",
    "大多数情况下，梯度带用于计算单个值（通常是损失）相对于一组值（通常是模型参数）的梯度。这就是反向模式 autodiff 的亮点，因为它只需要执行一次前向传递和一次反向传递即可一次获得所有梯度。如果您尝试计算向量的梯度，例如包含多个损失的向量，那么 TensorFlow 将计算向量总和的梯度。因此，如果您需要获得单独的梯度（例如，每个损失相对于模型参数的梯度），您必须调用磁带的 jacobian() 方法：它将为向量中的每个损失执行一次反向模式 autodiff （默认情况下全部并行）。甚至可以计算二阶偏导数（Hessians，即偏导数的偏导数），但这在实践中很少需要（例如，请参阅笔记本的“使用 Autodiff 计算梯度”部分） ."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "unlike-veteran",
   "metadata": {},
   "source": [
    "在某些情况下，您可能希望阻止梯度通过神经网络的某些部分进行反向传播。 为此，您必须使用 tf.stop_gradient() 函数。 该函数在前向传播期间返回其输入（如 tf.identity()），但在反向传播期间它不会让梯度通过（它就像一个常数）："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 58,
   "id": "dietary-vision",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[<tf.Tensor: shape=(), dtype=float32, numpy=30.0>, None]"
      ]
     },
     "execution_count": 58,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 11.2 ms (started: 2021-08-17 23:48:33 +08:00)\n"
     ]
    }
   ],
   "source": [
    "def f(w1, w2):\n",
    "    return 3 * w1 ** 2 + tf.stop_gradient(2 * w1 * w2)\n",
    "\n",
    "with tf.GradientTape() as tape: \n",
    "    z = f(w1, w2) # same result as without stop_gradient()\n",
    "    \n",
    "gradients = tape.gradient(z, [w1, w2]) # => returns [tensor 30., None]\n",
    "gradients"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "selective-continuity",
   "metadata": {},
   "source": [
    "最后，在计算梯度时，您可能偶尔会遇到一些数值问题。 例如，如果您为大输入计算 my_softplus() 函数的梯度，结果将为 NaN："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 59,
   "id": "disabled-plasma",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 10.4 ms (started: 2021-08-17 23:49:23 +08:00)\n"
     ]
    }
   ],
   "source": [
    "x = tf.Variable([100.])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 62,
   "id": "mature-growing",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[<tf.Tensor: shape=(1,), dtype=float32, numpy=array([nan], dtype=float32)>]"
      ]
     },
     "execution_count": 62,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 19.4 ms (started: 2021-08-17 23:50:13 +08:00)\n"
     ]
    }
   ],
   "source": [
    "with tf.GradientTape() as tape:\n",
    "    z = my_softplus(x)\n",
    "    \n",
    "tape.gradient(z, [x])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "talented-empire",
   "metadata": {},
   "source": [
    "这是因为使用 autodiff 计算此函数的梯度会导致一些数值困难：由于浮点精度错误，autodiff 最终计算无穷大除以无穷大（返回 NaN）。 幸运的是，我们可以通过解析发现 softplus 函数的导数仅为 1 / (1 + 1 / exp(x))，在数值上是稳定的。 接下来，我们可以告诉 TensorFlow 在计算 my_softplus() 函数的梯度时使用这个稳定函数，方法是用 @tf.custom_gradient 装饰它并使其返回其正常输出和计算导数的函数（注意它将接收 作为输入到目前为止反向传播的梯度，直到 softplus 函数；并且根据链式法则，我们应该将它们与该函数的梯度相乘）："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 63,
   "id": "divine-commitment",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 1.08 ms (started: 2021-08-17 23:51:36 +08:00)\n"
     ]
    }
   ],
   "source": [
    "@tf.custom_gradient\n",
    "def my_better_softplus(z):\n",
    "    exp = tf.exp(z)\n",
    "    def my_softplus_gradients(grad):\n",
    "        return grad / (1 + 1 / exp)\n",
    "    return tf.math.log(exp + 1), my_softplus_gradients"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "neural-dispatch",
   "metadata": {},
   "source": [
    "现在，当我们计算 my_better_softplus() 函数的梯度时，我们得到了正确的结果，即使对于较大的输入值（但是，由于指数，主输出仍然爆炸；一种解决方法是使用 tf.where() 返回 当它们很大时输入）。\n",
    "\n",
    "恭喜！ 您现在可以计算任何函数的梯度（前提是它在您计算的点是可微的），甚至在需要时阻止反向传播，并编写您自己的梯度函数！ 这可能比您需要的灵活性更大，即使您构建自己的自定义训练循环，正如我们现在将看到的。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "sacred-settlement",
   "metadata": {},
   "source": [
    "### 自定义训练循环"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "assigned-scheme",
   "metadata": {},
   "source": [
    "在极少数情况下，fit() 方法可能不够灵活，无法满足您的需求。 例如，我们在第 10 章中讨论的 Wide & Deep 论文使用了两种不同的优化器：一种用于宽路径，另一种用于深路径。 由于 fit() 方法仅使用一个优化器（我们在编译模型时指定的优化器），因此实现本文需要编写您自己的自定义循环。\n",
    "\n",
    "您可能还想编写自定义训练循环，只是为了让它们更加自信地执行您希望它们执行的操作（也许您不确定 fit() 方法的某些细节）。 有时让一切都变得明确会让人感觉更安全。 但是，请记住，编写自定义训练循环会使您的代码更长、更容易出错且更难维护。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fiscal-logan",
   "metadata": {},
   "source": [
    "> 除非你真的需要额外的灵活性，否则你应该更喜欢使用 fit() 方法而不是实现你自己的训练循环，特别是如果你在团队中工作。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "reserved-turtle",
   "metadata": {},
   "source": [
    "首先，让我们建立一个简单的模型。 无需编译它，因为我们将手动处理训练循环："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 64,
   "id": "major-speaker",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 23.9 ms (started: 2021-08-17 23:56:14 +08:00)\n"
     ]
    }
   ],
   "source": [
    "l2_reg = keras.regularizers.l2(0.05)\n",
    "model = keras.models.Sequential([keras.layers.Dense(30, \n",
    "                                                    activation=\"elu\", \n",
    "                                                    kernel_initializer=\"he_normal\",\n",
    "                                                    kernel_regularizer=l2_reg),\n",
    "                                 keras.layers.Dense(1, kernel_regularizer=l2_reg)\n",
    "                                ])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "prescribed-twenty",
   "metadata": {},
   "source": [
    "接下来，让我们创建一个小函数，从训练集中随机抽取一批实例（在第 13 章我们将讨论数据 API，它提供了一个更好的选择）："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 67,
   "id": "aboriginal-nelson",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 805 µs (started: 2021-08-18 00:01:40 +08:00)\n"
     ]
    }
   ],
   "source": [
    "def random_batch(X, y, batch_size=32):\n",
    "    idx = np.random.randint(len(X), size=batch_size)\n",
    "    return X[idx], y[idx]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "satisfactory-transportation",
   "metadata": {},
   "source": [
    "我们还定义一个函数来显示训练状态，包括步数、总步数、自 epoch 开始以来的平均损失（即我们将使用 Mean 度量来计算它）以及其他度量 ："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 68,
   "id": "billion-christianity",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 1.03 ms (started: 2021-08-18 00:01:42 +08:00)\n"
     ]
    }
   ],
   "source": [
    "def print_status_bar(iteration, total, loss, metrics=None):\n",
    "    metrics = \" - \".join([\"{}: {:.4f}\".format(m.name, m.result())\n",
    "                          for m in [loss] + (metrics or [])])\n",
    "        \n",
    "    end = \"\" if iteration < total else \"\\n\"\n",
    "        \n",
    "    print(\"\\r{}/{} - \".format(iteration, total) + metrics, end=end)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "postal-smoke",
   "metadata": {},
   "source": [
    "此代码是不言自明的，除非您不熟悉 Python 字符串格式：{:.4f} 将格式化小数点后四位数字的浮点数，并使用 \\r（回车）和 end=\"\" 确保 状态栏总是打印在同一行上。 在笔记本中，print_status_bar() 函数包含一个进度条，但您可以改用方便的 tqdm 库。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "palestinian-electricity",
   "metadata": {},
   "source": [
    "有了这个，让我们开始正事吧！ 首先，我们需要定义一些超参数并选择优化器、损失函数和指标（在这个例子中只是 MAE）："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 69,
   "id": "excellent-smooth",
   "metadata": {},
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'X_train' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[0;31mNameError\u001b[0m                                 Traceback (most recent call last)",
      "\u001b[0;32m<ipython-input-69-9abfe1ca5a0d>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[1;32m      1\u001b[0m \u001b[0mn_epochs\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;36m5\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m      2\u001b[0m \u001b[0mbatch_size\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;36m32\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 3\u001b[0;31m \u001b[0mn_steps\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mX_train\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m//\u001b[0m \u001b[0mbatch_size\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m      4\u001b[0m \u001b[0moptimizer\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mkeras\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0moptimizers\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mNadam\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mlr\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m0.01\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m      5\u001b[0m \u001b[0mloss_fn\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mkeras\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mlosses\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmean_squared_error\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
      "\u001b[0;31mNameError\u001b[0m: name 'X_train' is not defined"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 13.5 ms (started: 2021-08-18 00:01:46 +08:00)\n"
     ]
    }
   ],
   "source": [
    "n_epochs = 5\n",
    "batch_size = 32\n",
    "n_steps = len(X_train) // batch_size\n",
    "optimizer = keras.optimizers.Nadam(lr=0.01)\n",
    "loss_fn = keras.losses.mean_squared_error\n",
    "mean_loss = keras.metrics.Mean()\n",
    "metrics = [keras.metrics.MeanAbsoluteError()]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "active-lafayette",
   "metadata": {},
   "source": [
    "现在我们准备好构建自定义循环了！"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "excessive-content",
   "metadata": {},
   "outputs": [],
   "source": [
    "for epoch in range(1, n_epochs + 1):\n",
    "    print(\"Epoch {}/{}\".format(epoch, n_epochs))\n",
    "    \n",
    "    for step in range(1, n_steps + 1):\n",
    "        X_batch, y_batch = random_batch(X_train_scaled, y_train)\n",
    "        with tf.GradientTape() as tape:\n",
    "            y_pred = model(X_batch, training=True)\n",
    "            main_loss = tf.reduce_mean(loss_fn(y_batch, y_pred))\n",
    "            loss = tf.add_n([main_loss] + model.losses)\n",
    "        gradients = tape.gradient(loss, model.trainable_variables)\n",
    "        optimizer.apply_gradients(zip(gradients, model.trainable_variables))\n",
    "        mean_loss(loss)\n",
    "        for metric in metrics:\n",
    "            metric(y_batch, y_pred)\n",
    "        print_status_bar(step * batch_size, len(y_train), mean_loss, metrics)\n",
    "    print_status_bar(len(y_train), len(y_train), mean_loss, metrics)\n",
    "    for metric in [mean_loss] + metrics:\n",
    "        metric.reset_states()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "conservative-germany",
   "metadata": {},
   "source": [
    "这段代码中发生了很多事情，让我们来看看它：\n",
    "* 我们创建了两个嵌套循环：一个用于周期，另一个用于周期内的批次。\n",
    "* 然后我们从训练集中随机抽样。\n",
    "* 在 tf.GradientTape() 块内，我们对一批进行预测（使用模型作为函数），并计算损失：它等于主要损失加上其他损失（在这个模型中，有每层一个正则化损失）。由于 mean_squared_error() 函数为每个实例返回一个损失，我们使用 tf.reduce_mean() 计算批次的均值（如果您想对每个实例应用不同的权重，这就是您要做的地方）。每个正则化损失已经减少到一个标量，所以我们只需要对它们求和（使用 tf.add_n()，它对相同形状和数据类型的多个张量求和）。\n",
    "* 接下来，我们要求磁带计算每个可训练变量（不是所有变量！）的损失梯度，并将它们应用到优化器以执行梯度下降步骤。\n",
    "* 然后我们更新平均损失和指标（在当前时期内），并显示状态栏。\n",
    "* 在每个时期结束时，我们再次显示状态栏以使其看起来完整并打印换行符，并重置平均损失和指标的状态。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "local-reach",
   "metadata": {},
   "source": [
    "如果您设置优化器的 clipnorm 或 clipvalue 超参数，它会为您处理这些。 如果您想对渐变应用任何其他转换，只需在调用 apply_gradients() 方法之前执行此操作。 如果您向模型添加权重约束（例如，通过在创建层时设置 kernel_constraint 或 bias_constraint），您应该更新训练循环以在 apply_gradients() 之后应用这些约束："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "valid-auction",
   "metadata": {},
   "outputs": [],
   "source": [
    "for variable in model.variables:\n",
    "    if variable.constraint is not None:\n",
    "        variable.assign(variable.constraint(variable))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "tough-morning",
   "metadata": {},
   "source": [
    "在训练和测试期间（例如，BatchNormalization 或 Dropout）。 要处理这些，您需要使用 training=True 调用模型并确保它将其传播到需要它的每个层。\n",
    "\n",
    "如您所见，您需要做对很多事情，而且很容易出错。 但从好的方面来说，你可以完全控制，所以这是你的决定。\n",
    "\n",
    "既然您知道如何自定义模型的任何部分和训练算法，让我们看看如何使用 TensorFlow 的自动图形生成功能：它可以显着加快您的自定义代码的速度，并且还可以使其可移植到 TensorFlow 支持的任何平台 （见第 19 章）。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "assured-marking",
   "metadata": {},
   "source": [
    "## TensorFlow 函数和图形"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "loaded-indonesian",
   "metadata": {},
   "source": [
    "在 TensorFlow 1 中，图是不可避免的（以及随之而来的复杂性），因为它们是 TensorFlow API 的核心部分。 在 TensorFlow 2 中，它们仍然存在，但没有那么重要，而且它们使用起来（非常！）简单得多。 为了展示它有多简单，让我们从一个计算其输入的立方的普通函数开始："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 70,
   "id": "developmental-stopping",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 885 µs (started: 2021-08-18 00:12:04 +08:00)\n"
     ]
    }
   ],
   "source": [
    "def cube(x):\n",
    "    return x ** 3"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "sweet-vermont",
   "metadata": {},
   "source": [
    "显然，我们可以使用 Python 值调用此函数，例如 int 或 float，或者我们可以使用张量调用它："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 71,
   "id": "nasty-bottle",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "8"
      ]
     },
     "execution_count": 71,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 3.29 ms (started: 2021-08-18 00:12:47 +08:00)\n"
     ]
    }
   ],
   "source": [
    "cube(2)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 72,
   "id": "dominican-missouri",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<tf.Tensor: shape=(), dtype=float32, numpy=8.0>"
      ]
     },
     "execution_count": 72,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 7.19 ms (started: 2021-08-18 00:12:55 +08:00)\n"
     ]
    }
   ],
   "source": [
    "cube(tf.constant(2.0))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "alternative-membership",
   "metadata": {},
   "source": [
    "现在，让我们使用 tf.function() 将此 Python 函数转换为 TensorFlow 函数："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 73,
   "id": "sound-surface",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<tensorflow.python.eager.def_function.Function at 0x7fa5ecd78cd0>"
      ]
     },
     "execution_count": 73,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 3.88 ms (started: 2021-08-18 00:13:28 +08:00)\n"
     ]
    }
   ],
   "source": [
    "tf_cube = tf.function(cube)\n",
    "tf_cube"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "little-insurance",
   "metadata": {},
   "source": [
    "然后可以像原始 Python 函数一样使用这个 TF 函数，它将返回相同的结果（但作为张量）："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 74,
   "id": "equipped-florida",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<tf.Tensor: shape=(), dtype=int32, numpy=8>"
      ]
     },
     "execution_count": 74,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 102 ms (started: 2021-08-18 00:14:00 +08:00)\n"
     ]
    }
   ],
   "source": [
    "tf_cube(2)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 75,
   "id": "built-uniform",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<tf.Tensor: shape=(), dtype=float32, numpy=8.0>"
      ]
     },
     "execution_count": 75,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 32.7 ms (started: 2021-08-18 00:14:08 +08:00)\n"
     ]
    }
   ],
   "source": [
    "tf_cube(tf.constant(2.0))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "offensive-energy",
   "metadata": {},
   "source": [
    "在幕后，tf.function() 分析了由 cube() 函数执行的计算并生成了等效的计算图！ 正如您所看到的，这是相当轻松的（我们很快就会看到它是如何工作的）。 或者，我们可以使用 tf.function 作为装饰器； 这实际上更常见："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 76,
   "id": "minor-yukon",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 968 µs (started: 2021-08-18 00:15:21 +08:00)\n"
     ]
    }
   ],
   "source": [
    "@tf.function\n",
    "def tf_cube(x):\n",
    "    return x ** 3"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "removable-darkness",
   "metadata": {},
   "source": [
    "原始 Python 函数仍可通过 TF 函数的 python_function 属性获得，以防您需要它："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 77,
   "id": "stock-bhutan",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "8"
      ]
     },
     "execution_count": 77,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 2.67 ms (started: 2021-08-18 00:15:50 +08:00)\n"
     ]
    }
   ],
   "source": [
    "tf_cube.python_function(2)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bottom-acrobat",
   "metadata": {},
   "source": [
    "TensorFlow 优化计算图、修剪未使用的节点、简化表达式（例如，1 + 2 将被 3 替换）等等。 一旦优化的图准备好，TF 函数就会以适当的顺序（并在可能的情况下并行）有效地执行图中的操作。 因此，TF 函数通常会比原始 Python 函数运行得快得多，尤其是在执行复杂计算时。 大多数情况下，您实际上不需要知道更多：当您想要提升 Python 函数时，只需将其转换为 TF 函数即可。 就这样！"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "third-albany",
   "metadata": {},
   "source": [
    "此外，当您编写自定义损失函数、自定义指标、自定义层或任何其他自定义函数并在 Keras 模型中使用它时（正如我们在本章中所做的那样），Keras 会自动将您的函数转换为 TF 函数—— 无需使用 tf.function()。 所以大多数时候，所有这些魔法都是 100% 透明的。·"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "spiritual-minute",
   "metadata": {},
   "source": [
    "> 您可以通过在创建自定义层或自定义模型时设置 dynamic=True 来告诉 Keras 不要将 Python 函数转换为 TF 函数。 或者，您可以在调用模型的 compile() 方法时设置 run_eagerly=True。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "pregnant-feelings",
   "metadata": {},
   "source": [
    "默认情况下，TF 函数为每组唯一的输入形状和数据类型生成一个新图，并将其缓存以供后续调用。 例如，如果您调用 tf_cube(tf.constant(10))，将为形状为 [] 的 int32 张量生成一个图。 然后，如果您调用 tf_cube(tf.constant(20))，将重用相同的图形。 但是，如果您随后调用 tf_cube(tf.constant([10, 20]))，将为形状为 [2] 的 int32 张量生成一个新图。 这就是 TF 函数处理多态性的方式（即，不同的参数类型和形状）。 但是，这仅适用于张量参数：如果将 Python 数值传递给 TF 函数，将为每个不同的值生成一个新图：例如，调用 tf_cube(10) 和 tf_cube(20) 将生成两个图。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cloudy-cedar",
   "metadata": {},
   "source": [
    "如果您使用不同的 Python 数值多次调用 TF 函数，则会生成许多图形，从而减慢您的程序速度并占用大量 RAM（您必须删除 TF 函数以释放它）。 Python 值应保留用于几乎没有唯一值的参数，例如每层神经元数量等超参数。 这允许 TensorFlow 更好地优化模型的每个变体。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "boolean-helicopter",
   "metadata": {},
   "source": [
    "### Autograph和跟踪"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "laughing-point",
   "metadata": {},
   "source": [
    "那么TensorFlow是如何生成图的呢？ 它首先分析 Python 函数的源代码以捕获所有控制流语句，例如 for 循环、while 循环和 if 语句，以及 break、continue 和 return 语句。 第一步称为 AutoGraph。 TensorFlow 必须分析源代码的原因是 Python 没有提供任何其他方法来捕获控制流语句：它提供了像 __add__() 和 __mul__() 这样的魔术方法来捕获像 + 和 * 这样的运算符，但是没有 __while__( ) 或 __if__() 魔术方法。 在分析函数的代码后，AutoGraph 输出该函数的升级版本，其中所有控制流语句都被适当的 TensorFlow 操作替换，例如用于循环的 tf.while_loop() 和用于 if 语句的 tf.cond()。 例如，在图 12-4 中，AutoGraph 分析了 sum_squares() Python 的源代码"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "respiratory-short",
   "metadata": {},
   "source": [
    "那么TensorFlow是如何生成图的呢？它首先分析 Python 函数的源代码以捕获所有控制流语句，例如 for 循环、while 循环和 if 语句，以及 break、continue 和 return 语句。第一步称为 AutoGraph。 TensorFlow 必须分析源代码的原因是 Python 没有提供任何其他方法来捕获控制流语句：它提供了像 __add__() 和 __mul__() 这样的魔术方法来捕获像 + 和 * 这样的运算符，但是没有 __while__( ) 或 __if__() 魔术方法。在分析函数的代码后，AutoGraph 输出该函数的升级版本，其中所有控制流语句都被适当的 TensorFlow 操作替换，例如用于循环的 tf.while_loop() 和用于 if 语句的 tf.cond()。例如，在图 12-4 中，AutoGraph 分析了 sum_squares() Python 函数的源代码，并生成了 tf__sum_squares() 函数。在这个函数中，for 循环被 loop_body() 函数的定义所替代（包含原始 for 循环的主体），然后调用 for_stmt() 函数。此调用将在计算图中构建适当的 tf.while_loop() 操作。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "positive-concentration",
   "metadata": {},
   "source": [
    "![](https://tva1.sinaimg.cn/large/008i3skNgy1gtk8wmzacpj61dc0t2wj502.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "eastern-leeds",
   "metadata": {},
   "source": [
    "接下来，TensorFlow 调用这个“升级”的函数，但它不传递参数，而是传递一个符号张量——一个没有任何实际值、只有名称、数据类型和形状的张量。例如，如果您调用 sum_squares(tf.constant(10))，那么 tf__sum_squares() 函数将使用 int32 类型和形状 [] 的符号张量调用。该函数将在图形模式下运行，这意味着每个 TensorFlow 操作都会在图形中添加一个节点来表示其自身及其输出张量（与常规模式相反，称为急切执行或急切模式）。在图形模式下，TF 操作不执行任何计算。如果您了解 TensorFlow 1，这应该会很熟悉，因为图形模式是默认模式。在图 12-4 中，您可以看到使用符号张量作为参数调用 tf__sum_squares() 函数（在本例中，形状为 [] 的 int32 张量）以及在跟踪期间生成的最终图形。节点代表操作，箭头代表张量（生成的函数和图都简化了）。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "headed-hepatitis",
   "metadata": {},
   "source": [
    "> 要查看生成函数的源代码，您可以调用 tf.autograph.to_code(sum_squares.python_function)。 代码并不意味着漂亮，但它有时可以帮助调试。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "olympic-parks",
   "metadata": {},
   "source": [
    "### TF 函数规则"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bigger-copying",
   "metadata": {},
   "source": [
    "大多数情况下，将执行 TensorFlow 操作的 Python 函数转换为 TF 函数是微不足道的：用 @tf.function 装饰它或让 Keras 为您处理它。 但是，有一些规则需要遵守：\n",
    "* 如果您调用任何外部库，包括 NumPy 甚至标准库，此调用将仅在跟踪期间运行； 它不会成为图表的一部分。 事实上，一个 TensorFlow 图只能包含 TensorFlow 结构（张量、操作、变量、数据集等）。 因此，请确保使用 tf.reduce_sum() 代替 np.sum()，使用 tf.sort() 代替内置的 sorted() 函数，等等（除非您真的希望代码仅在跟踪期间运行 ）。 这有一些额外的含义：\n",
    "    - 如果定义了一个只返回 np.random.rand() 的 TF 函数 f(x)，则只有在跟踪该函数时才会生成一个随机数，因此 f(tf.constant(2.)) 和 f(tf) .constant(3.)) 将返回相同的随机数，但 f(tf.constant([2., 3.])) 将返回不同的随机数。如果将 np.random.rand() 替换为 tf.random.uniform([])，则每次调用时都会生成一个新的随机数，因为该操作将是图形的一部分。\n",
    "    - 如果您的非 TensorFlow 代码有副作用（例如记录某些内容或更新 Python 计数器），那么您不应期望每次调用 TF 函数时都会发生这些副作用，因为它们只会在跟踪函数时发生.\n",
    "    - 您可以在 tf.py_function() 操作中包装任意 Python 代码，但这样做会影响性能，因为 TensorFlow 将无法对此代码进行任何图形优化。它还会降低可移植性，因为该图只会在 Python 可用的平台上运行（并且安装了正确的库）。\n",
    "* 您可以调用其他 Python 函数或 TF 函数，但它们应遵循相同的规则，因为 TensorFlow 将在计算图中捕获它们的操作。请注意，这些其他函数不需要用@tf.function 修饰。\n",
    "* 如果该函数创建了一个 TensorFlow 变量（或任何其他有状态的 TensorFlow 对象，例如数据集或队列），则它必须在第一次调用时执行此操作，并且只有在此之后，否则您将收到异常。通常最好在 TF 函数之外创建变量（例如，在自定义层的 build() 方法中）。如果要为变量分配新值，请确保调用其assign() 方法，而不是使用= 运算符。\n",
    "* 您的 Python 函数的源代码应该可供 TensorFlow 使用。如果源代码不可用（例如，如果您在 Python shell 中定义了您的函数，但它不会提供对源代码的访问权限，或者如果您仅将编译后的 *.pyc Python 文件部署到生产环境中），则图形生成进程将失败或功能有限。\n",
    "* TensorFlow 将仅捕获迭代张量或数据集的 for 循环。因此，请确保在 tf.range(x) 中使用 for i 而不是 for i in range(x)，否则循环将不会在图中捕获。相反，它将在跟踪期间运行。 （如果 for 循环旨在构建图形，例如在神经网络中创建每一层，这可能是您想要的。）\n",
    "* 一如既往，出于性能原因，您应该尽可能选择矢量化实现，而不是使用循环。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "above-validity",
   "metadata": {},
   "source": [
    "然后我们查看了 TensorFlow 的低级 API，包括张量、操作、变量和特殊数据结构。 然后我们使用这些工具来自定义 tf.keras 中的几乎每个组件。 最后，我们研究了 TF 函数如何提高性能，如何使用 AutoGraph 和跟踪生成图形，以及编写 TF 函数时应遵循的规则（如果您想进一步打开黑匣子，例如探索 生成的图表，您将在附录 G 中找到技术细节）。\n",
    "\n",
    "在下一章中，我们将看看如何使用 TensorFlow 高效地加载和预处理数据。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "social-mattress",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python [conda env:tensorflow]",
   "language": "python",
   "name": "conda-env-tensorflow-py"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.10"
  },
  "toc-autonumbering": true
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
